「英雄之旅」已經可以瀏覽完整的英雄列表,並透過路由參數來取得特定的英雄資料,達到換頁瀏覽細節資訊的功能,可以說,我們初步完成了英雄的資料查詢。在我們進一步完成對資料的增、刪、改之前,先要來重構程式碼:將元件中與資料互動的相關邏輯移走——讓元件專注在展示資料上,而關於與資料互動的邏輯,我們將新增服務(service)來統一管理。
在一般的情境下,一個服務應該是整個 app 共用的。如此一來,不管這個服務注入在哪個元件中(被哪個元件、功能使用),都能夠確保它的狀態(資料)是一致的。因此,我們在 shared 資料夾下新增 services 資料夾來管理 service 檔案,並在此資料夾執行指令:
ng g s hero // g for generate; s for service
檔案目錄如下:
src
⌞app
⌞ shared
⌞ models
⌞ services
hero.service.ts
打開 hero.service.ts
檔案:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class HeroService {
constructor() { }
}
可以注意到服務其實也是個類別(class),此外,@Injectable
裝飾器是非常重要的,因為:
private http: HttpClient
(當然還需要在上面 import HttpClient):import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class HeroService {
constructor(
private http: HttpClient
) { }
}
@Injectable({
providedIn: 'root' // 應用程式級服務,確保注入此服務的地方,狀態(資料)是一致的。
})
目前在 App 中,擁有兩個與資料互動的地方:
先在 hero.service.ts
來撰寫取得所有英雄資料的方法:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Hero } from './../models/hero.model';
@Injectable({
providedIn: 'root'
})
export class HeroService {
constructor(
private http: HttpClient
) { }
getHeroes(): Observable<Hero[]> {
return this.http.get<Hero[]>(`api/heroes`);
}
我們主要關注 getHeroes():
getHeroes(): Observable<Hero[]> {
return this.http.get<Hero[]>(`api/heroes`);
}
可以看到,這個方法會回傳的資料型態是 Observable<Hero[]>
,Observable 是 RxJS 的術語,意思是可觀察的。在 Angular,使用 HttpClient 的 get 方法回傳的資料預設都是 Observable。
接著,在 hero-list.component.ts
依賴注入 HeroService,並調整取得所有英雄資料的方法:
import { HeroService } from './../shared/services/hero.service';
import { Component, OnInit } from '@angular/core';
import { Hero } from './../shared/models/hero.model';
@Component({
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
styleUrls: ['./hero-list.component.css']
})
export class HeroListComponent implements OnInit {
heroList: Hero[] = [];
constructor(
private heroService: HeroService
) {}
ngOnInit(): void {
this.heroService.getHeroes().subscribe((heroList) => {
this.heroList = heroList;
})
}
}
程式碼幾乎相同,但取得資料的種種邏輯被移到 HeroService 了——我們不必在 HeroListComponent 知道這些資訊。
除了上面的使用方法之外,因為 HttpClient 回傳的資料是可觀察的(Observable),因此我們可以使用下列的方法來實作。首先調整 hero-list.component.ts
:
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { HeroService } from './../shared/services/hero.service';
import { Hero } from './../shared/models/hero.model';
@Component({
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
styleUrls: ['./hero-list.component.css']
})
export class HeroListComponent implements OnInit {
heroList$: Observable<Hero[]>;
constructor(
private heroService: HeroService
) {
this.heroList$ = this.heroService.getHeroes();
}
ngOnInit(): void {}
}
我們將原本的屬性 heroList: Hero[]
調整為 heroList$: Observable<Hero[]>
,$
是 RxJS 的慣用寫法,代表這是一個可以被觀察(訂閱)的屬性——這裡也可以注意到,原本在 hero-list.component.ts
中的訂閱(subscribe)資料的行為消失了。因為,我們將使用 Angular 提供的 Async 管道來訂閱它,讓我們在畫面檔案 hero-list.component.html
來完成這件事:
<div class="hero-container" *ngIf="heroList$ | async as heroList">
<mat-card class="hero-item" *ngFor="let hero of heroList">
(略)
</mat-card>
</div>
在 div 上我們使用了 *ngIf 指令,放置 heroList$ 屬性並使用 async 管道,這個管道將訂閱 heroList$,也就是 HeroService 服務的 getHeroes()
回傳的資料:
getHeroes(): Observable<Hero[]> {
return this.http.get<Hero[]>(`api/heroes`);
}
當有接收到資料時,*ngIf 就會為 true,以下的所有標籤就會建立出來(顯示在畫面上)。同時,得到的資料被指派為變數 heroList (as heroList)。這與原先的程式碼是相同的(*ngFor="let hero of heroList"
),因此其他程式碼並不需要改動。
藉由將 getHeroes()
移到 HeroService 並改為 RxJS 的寫法,我們的程式碼更為精簡。而另外一個方法 getHero(heroId)
將更深入地使用到 RxJS 的 operator,這是我們明天要來完成的事:「極簡 RxJS 使用方法」。打完這幾個字我自己都抖了起來,趕快睡覺壓壓驚 :P。
程式碼已推上 Github。